﻿//------------------------------------------------------------------------------
//  此代码版权声明为全文件覆盖，如有原作者特别声明，会在下方手动补充
//  此代码版权（除特别声明外的代码）归作者本人Diego所有
//  源代码使用协议遵循本仓库的开源协议及附加协议
//  Gitee源代码仓库：https://gitee.com/diego2098/ThingsGateway
//  Github源代码仓库：https://github.com/kimdiego2098/ThingsGateway
//  使用文档：https://thingsgateway.cn/
//  QQ群：605534569
//------------------------------------------------------------------------------

//修改自https://github.com/dathlin/OpcUaHelper 与OPC基金会net库

using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif

namespace ThingsGateway.Foundation.OpcUa;

/// <summary>
/// 订阅委托
/// </summary>
/// <param name="value"></param>
public delegate void DataChangedEventHandler((VariableNode variableNode, MonitoredItem monitoredItem, DataValue dataValue, JToken jToken) value);

/// <summary>
/// 日志输出
/// </summary>
public delegate void LogEventHandler(byte level, object sender, string message, Exception ex);

/// <summary>
/// OpcUaMaster
/// </summary>
public class OpcUaMaster : IAsyncDisposable
{
    #region 属性，变量等

    /// <summary>
    /// Raised after the client status change
    /// </summary>
    public LogEventHandler LogEvent;

    /// <summary>
    /// 当前配置
    /// </summary>
    public OpcUaProperty OpcUaProperty;

    /// <summary>
    /// ProductUri
    /// </summary>
    public string ProductUri = "https://thingsgateway.cn/";

    /// <summary>
    /// 当前的变量名称/OPC变量节点
    /// </summary>
    private readonly ConcurrentDictionary<string, VariableNode> _variableDicts = new();

    private readonly object checkLock = new();

    /// <summary>
    /// 当前的订阅组，组名称/组
    /// </summary>
    private readonly ConcurrentDictionary<string, Subscription> _subscriptionDicts = new();

    private readonly ApplicationInstance m_application = new();

    private readonly ApplicationConfiguration m_configuration;
    private EventHandler<bool> m_ConnectComplete;
    private EventHandler<KeepAliveEventArgs> m_KeepAliveComplete;
    private EventHandler m_ReconnectComplete;
    private SessionReconnectHandler m_reConnectHandler;
    private EventHandler m_ReconnectStarting;
    private ISession m_session;

    private ComplexTypeSystem typeSystem;

    /// <summary>
    /// 默认的构造函数，实例化一个新的OPC UA类
    /// </summary>
    public OpcUaMaster()
    {
        var certificateValidator = new CertificateValidator();
        certificateValidator.CertificateValidation += CertificateValidation;
        // 构建应用程序配置
        m_configuration = new ApplicationConfiguration
        {
            ApplicationName = OPCUAName,
            ApplicationType = ApplicationType.Client,
            CertificateValidator = certificateValidator,
            ApplicationUri = Utils.Format(@"urn:{0}:{1}", System.Net.Dns.GetHostName(), OPCUAName),
            ProductUri = ProductUri,

            ServerConfiguration = new ServerConfiguration
            {
                MaxSubscriptionCount = 100000,
                MaxMessageQueueSize = 1000000,
                MaxNotificationQueueSize = 1000000,
                MaxPublishRequestCount = 10000000,
            },

            SecurityConfiguration = new SecurityConfiguration
            {
                UseValidatedCertificates = true,
                AutoAcceptUntrustedCertificates = true,//自动接受证书
                RejectSHA1SignedCertificates = true,
                AddAppCertToTrustedStore = true,
                SendCertificateChain = true,
                MinimumCertificateKeySize = 1024,
                SuppressNonceValidationErrors = true,

                ApplicationCertificate = new CertificateIdentifier
                {
                    StoreType = CertificateStoreType.X509Store,
                    StorePath = "CurrentUser\\" + OPCUAName,
                    SubjectName = $"CN={OPCUAName}, C=CN, S=GUANGZHOU, O=ThingsGateway, DC=" + System.Net.Dns.GetHostName(),
                },

                TrustedIssuerCertificates = new CertificateTrustList
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\trustedIssuer",
                },
                TrustedPeerCertificates = new CertificateTrustList
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\trustedPeer",
                },
                RejectedCertificateStore = new CertificateStoreIdentifier
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\rejected",
                },
                UserIssuerCertificates = new CertificateTrustList
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\issuerUser",
                },
                TrustedUserCertificates = new CertificateTrustList
                {
                    StoreType = CertificateStoreType.Directory,
                    StorePath = AppContext.BaseDirectory + @"OPCUAClientCertificate\pki\trustedUser",
                }
            },

            TransportQuotas = new TransportQuotas
            {
                OperationTimeout = 6000000,
                MaxStringLength = int.MaxValue,
                MaxByteStringLength = int.MaxValue,
                MaxArrayLength = 65535,
                MaxMessageSize = 419430400,
                MaxBufferSize = 65535,
                ChannelLifetime = 300000,
                SecurityTokenLifetime = 3600000
            },
            ClientConfiguration = new ClientConfiguration
            {
                DefaultSessionTimeout = 60000,
                MinSubscriptionLifetime = 10000,
            },
        };

        Task.Run(() => certificateValidator.UpdateAsync(m_configuration)).GetAwaiter().GetResult();

        Task.Run(() => m_configuration.ValidateAsync(ApplicationType.Client)).GetAwaiter().GetResult();
        m_application.ApplicationConfiguration = m_configuration;
    }

    /// <summary>
    /// Raised after successfully connecting to or disconnecing from a server.
    /// </summary>
    public event EventHandler<bool> ConnectComplete
    {
        add { m_ConnectComplete += value; }
        remove { m_ConnectComplete -= value; }
    }

    /// <summary>
    /// 订阅
    /// </summary>
    public event DataChangedEventHandler DataChangedHandler;

    /// <summary>
    /// Raised when a good keep alive from the server arrives.
    /// </summary>
    public event EventHandler<KeepAliveEventArgs> KeepAliveComplete
    {
        add { m_KeepAliveComplete += value; }
        remove { m_KeepAliveComplete -= value; }
    }

    /// <summary>
    /// Raised when a reconnect operation completes.
    /// </summary>
    public event EventHandler ReconnectComplete
    {
        add { m_ReconnectComplete += value; }
        remove { m_ReconnectComplete -= value; }
    }

    /// <summary>
    /// Raised when a reconnect operation starts.
    /// </summary>
    public event EventHandler ReconnectStarting
    {
        add { m_ReconnectStarting += value; }
        remove { m_ReconnectStarting -= value; }
    }

    /// <summary>
    /// 配置信息
    /// </summary>
    public ApplicationConfiguration AppConfig => m_configuration;


    /// <summary>
    /// 连接状态
    /// </summary>
    public bool Connected => m_session?.Connected == true && connected;

    private bool connected = false;

    /// <summary>
    /// OpcUaMaster
    /// </summary>
    public string OPCUAName { get; set; } = "ThingsGateway";

    /// <summary>
    /// SessionReconnectHandler
    /// </summary>
    public SessionReconnectHandler ReConnectHandler => m_reConnectHandler;

    /// <summary>
    /// 当前活动会话。
    /// </summary>
    public ISession Session => m_session;

    #endregion 属性，变量等

    #region 订阅

    /// <summary>
    /// 新增订阅，需要指定订阅组名称，订阅的tag名数组
    /// </summary>
    public async Task AddSubscriptionAsync(string subscriptionName, string[] items, bool loadType = true, CancellationToken cancellationToken = default)
    {
        Subscription m_subscription = new(m_session.DefaultSubscription)
        {
            PublishingEnabled = true,
            PublishingInterval = 0,
            KeepAliveCount = uint.MaxValue,
            LifetimeCount = uint.MaxValue,
            MaxNotificationsPerPublish = uint.MaxValue,
            Priority = 100,
            DisplayName = subscriptionName
        };
        List<MonitoredItem> monitoredItems = new();
        var variableNodes = loadType ? await ReadNodesAsync(items, false, cancellationToken).ConfigureAwait(false) : null;
        for (int i = 0; i < items.Length; i++)
        {
            try
            {
                var item = new MonitoredItem
                {
                    StartNodeId = items[i],
                    AttributeId = Attributes.Value,
                    DisplayName = items[i],
                    Filter = OpcUaProperty.DeadBand == 0 ? null : new DataChangeFilter() { DeadbandValue = OpcUaProperty.DeadBand, DeadbandType = (int)DeadbandType.Absolute, Trigger = DataChangeTrigger.StatusValue },
                    SamplingInterval = OpcUaProperty?.UpdateRate ?? 1000,
                };
                item.Notification += Callback;
                monitoredItems.Add(item);
            }
            catch (Exception ex)
            {
                Log(3, ex, $"Failed to initialize {items[i]} variable subscription");
            }
        }

        m_session.AddSubscription(m_subscription);
        try
        {
            await m_subscription.CreateAsync(cancellationToken).ConfigureAwait(false);
            await Task.Delay(100, cancellationToken).ConfigureAwait(false); // allow for subscription to be finished on server?
        }
        catch (Exception)
        {
            try
            {
                await m_subscription.CreateAsync(cancellationToken).ConfigureAwait(false);
                await Task.Delay(100, cancellationToken).ConfigureAwait(false); // allow for subscription to be finished on server?
            }
            catch (Exception)
            {
                await m_session.RemoveSubscriptionAsync(m_subscription, cancellationToken).ConfigureAwait(false);
                throw;
            }
        }

        m_subscription.AddItems(monitoredItems);
        foreach (var item in m_subscription.MonitoredItems.Where(a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode)))
        {
            item.Filter = OpcUaProperty.DeadBand == 0 ? null : new DataChangeFilter() { DeadbandValue = OpcUaProperty.DeadBand, DeadbandType = (int)DeadbandType.None, Trigger = DataChangeTrigger.StatusValue };
        }

        await m_subscription.ApplyChangesAsync(cancellationToken).ConfigureAwait(false);
        var isError = m_subscription.MonitoredItems.Any(a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode));
        if (isError)
        {
            Log(3, null, $"Failed to create subscription for the following variables：{Environment.NewLine}{m_subscription.MonitoredItems.Where(
                a => a.Status.Error != null && StatusCode.IsBad(a.Status.Error.StatusCode))
                .Select(a => $"{a.StartNodeId}：{a.Status.Error}").ToJsonString()}");
        }

        if (_subscriptionDicts.TryAdd(subscriptionName, m_subscription))
        {
        }
        else if (_subscriptionDicts.TryGetValue(subscriptionName, out var existingSubscription))
        {
            // remove
            await existingSubscription.DeleteAsync(true, cancellationToken).ConfigureAwait(false);
            await m_session.RemoveSubscriptionAsync(existingSubscription, cancellationToken).ConfigureAwait(false);
            try { existingSubscription.Dispose(); } catch { }
            _subscriptionDicts[subscriptionName] = m_subscription;
        }
    }

    /// <summary>
    /// 浏览一个节点的引用
    /// </summary>
    /// <param name="tag">节点值</param>
    /// <returns>引用节点描述</returns>
    public async Task<ReferenceDescription[]> BrowseNodeReferenceAsync(string tag)
    {
        NodeId sourceId = new(tag);

        // 该节点可以读取到方法
        BrowseDescription nodeToBrowse1 = new()
        {
            NodeId = sourceId,
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.Aggregates,
            IncludeSubtypes = true,
            NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable | NodeClass.Method),
            ResultMask = (uint)BrowseResultMask.All
        };

        // find all nodes organized by the node.
        BrowseDescription nodeToBrowse2 = new()
        {
            NodeId = sourceId,
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.Organizes,
            IncludeSubtypes = true,
            NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable),
            ResultMask = (uint)BrowseResultMask.All
        };

        BrowseDescriptionCollection nodesToBrowse = new()
        {
            nodeToBrowse1,
            nodeToBrowse2
        };

        // fetch references from the server.
        ReferenceDescriptionCollection references = await OpcUaUtils.BrowseAsync(m_session, nodesToBrowse, false).ConfigureAwait(false);

        return references.ToArray();
    }

    /// <summary>
    /// 调用服务器的方法
    /// </summary>
    /// <param name="tagParent">方法的父节点tag</param>
    /// <param name="tag">方法的节点tag</param>
    /// <param name="cancellationToken">cancellationToken</param>
    /// <param name="args">传递的参数</param>
    /// <returns>输出的结果值</returns>
    public async Task<IList<object>> CallMethodByNodeIdAsync(string tagParent, string tag, CancellationToken cancellationToken, params object[] args)
    {
        if (m_session == null)
        {
            return null;
        }

        IList<object> outputArguments = await m_session.CallAsync(
            new NodeId(tagParent),
            new NodeId(tag),
            cancellationToken,
            args).ConfigureAwait(false);

        return outputArguments.ToArray();
    }

    /// <summary>
    /// 连接到服务器
    /// </summary>
    public Task ConnectAsync(CancellationToken cancellationToken)
    {
        return ConnectAsync(OpcUaProperty.OpcUrl, cancellationToken);
    }

    /// <summary>
    /// 断开连接。
    /// </summary>
    public Task DisconnectAsync()
    {
        return PrivateDisconnectAsync();
    }

    public async ValueTask DisposeAsync()
    {
        await DisconnectAsync().ConfigureAwait(false);
        _variableDicts?.Clear();
        _subscriptionDicts?.Clear();
        waitLock?.Dispose();
    }
    /// <summary>
    /// 获取变量说明
    /// </summary>
    /// <returns></returns>
    public string GetAddressDescription()
    {
        return "ItemId";
    }

    /// <summary>
    /// 读取历史数据
    /// </summary>
    /// <param name="tag">节点的索引</param>
    /// <param name="start">开始时间</param>
    /// <param name="end">结束时间</param>
    /// <param name="count">读取的个数</param>
    /// <param name="containBound">是否包含边界</param>
    /// <param name="cancellationToken">cancellationToken</param>
    /// <returns>读取的数据列表</returns>
    public async Task<List<DataValue>> ReadHistoryRawDataValues(string tag, DateTime start, DateTime end, uint count = 1, bool containBound = false, CancellationToken cancellationToken = default)
    {
        HistoryReadValueId m_nodeToContinue = new()
        {
            NodeId = new NodeId(tag),
        };

        ReadRawModifiedDetails m_details = new()
        {
            StartTime = start,
            EndTime = end,
            NumValuesPerNode = count,
            IsReadModified = false,
            ReturnBounds = containBound
        };

        HistoryReadValueIdCollection nodesToRead = new()
        {
            m_nodeToContinue
        };

        var result = await m_session.HistoryReadAsync(
             null,
             new ExtensionObject(m_details),
             TimestampsToReturn.Both,
             false,
             nodesToRead,
             cancellationToken).ConfigureAwait(false);
        var results = result.Results;
        var diagnosticInfos = result.DiagnosticInfos;
        ClientBase.ValidateResponse(results, nodesToRead);
        ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);

        if (StatusCode.IsBad(results[0].StatusCode))
        {
            throw new ServiceResultException(results[0].StatusCode);
        }

        HistoryData values = ExtensionObject.ToEncodeable(results[0].HistoryData) as HistoryData;
        return values.DataValues;
    }

    /// <summary>
    /// 从服务器读取值
    /// </summary>
    public async Task<List<(string, DataValue, JToken)>> ReadJTokenValueAsync(string[] tags, CancellationToken cancellationToken = default)
    {
        var result = await ReadJTokenValueAsync(tags.Select(a => new NodeId(a)).ToArray(), cancellationToken).ConfigureAwait(false);
        return result;
    }

    /// <summary>
    /// 读取一个节点的所有属性
    /// </summary>
    public async Task<List<OPCNodeAttribute>> ReadNoteAttributeAsync(string tag, uint attributesId, CancellationToken cancellationToken = default)
    {
        BrowseDescriptionCollection nodesToBrowse = new();
        ReadValueIdCollection nodesToRead = new();
        NodeId sourceId = new(tag);

        ReadValueId nodeToRead = new()
        {
            NodeId = sourceId,
            AttributeId = attributesId
        };
        nodesToRead.Add(nodeToRead);
        BrowseDescription nodeToBrowse = new()
        {
            NodeId = sourceId,
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.HasProperty,
            IncludeSubtypes = true,
            NodeClassMask = 0,
            ResultMask = (uint)BrowseResultMask.All
        };
        nodesToBrowse.Add(nodeToBrowse);

        var result1 = await ReadNoteAttributeAsync(nodesToBrowse, nodesToRead, cancellationToken).ConfigureAwait(false);

        var result2 = result1.Values.FirstOrDefault();
        return result2;
    }

    /// <summary>
    /// 读取节点的所有属性
    /// </summary>
    public async Task<Dictionary<string, List<OPCNodeAttribute>>> ReadNoteAttributeAsync(List<string> tags, CancellationToken cancellationToken)
    {
        BrowseDescriptionCollection nodesToBrowse = new();
        ReadValueIdCollection nodesToRead = new();
        foreach (var tag in tags)
        {
            NodeId sourceId = new(tag);

            for (uint ii = Attributes.NodeClass; ii <= Attributes.UserExecutable; ii++)
            {
                ReadValueId nodeToRead = new()
                {
                    NodeId = sourceId,
                    AttributeId = ii
                };
                nodesToRead.Add(nodeToRead);
            }
            BrowseDescription nodeToBrowse = new()
            {
                NodeId = sourceId,
                BrowseDirection = BrowseDirection.Forward,
                ReferenceTypeId = ReferenceTypeIds.HasProperty,
                IncludeSubtypes = true,
                NodeClassMask = 0,
                ResultMask = (uint)BrowseResultMask.All
            };
            nodesToBrowse.Add(nodeToBrowse);
        }

        return await ReadNoteAttributeAsync(nodesToBrowse, nodesToRead, cancellationToken).ConfigureAwait(false);
    }

    /// <summary>
    /// 读取一个节点的所有属性
    /// </summary>
    /// <param name="tag">节点信息</param>
    /// <param name="cancellationToken">cancellationToken</param>
    /// <returns>节点的特性值</returns>
    public async Task<OPCNodeAttribute[]> ReadNoteAttributesAsync(string tag, CancellationToken cancellationToken)
    {
        NodeId sourceId = new(tag);
        ReadValueIdCollection nodesToRead = new();

        for (uint ii = Attributes.NodeClass; ii <= Attributes.UserExecutable; ii++)
        {
            ReadValueId nodeToRead = new()
            {
                NodeId = sourceId,
                AttributeId = ii
            };
            nodesToRead.Add(nodeToRead);
        }

        int startOfProperties = nodesToRead.Count;

        // find all of the pror of the node.
        BrowseDescription nodeToBrowse1 = new()
        {
            NodeId = sourceId,
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.HasProperty,
            IncludeSubtypes = true,
            NodeClassMask = 0,
            ResultMask = (uint)BrowseResultMask.All
        };

        BrowseDescriptionCollection nodesToBrowse = new()
        {
            nodeToBrowse1
        };

        // fetch property references from the server.
        ReferenceDescriptionCollection references = await OpcUaUtils.BrowseAsync(m_session, nodesToBrowse, false, cancellationToken).ConfigureAwait(false);

        if (references == null)
        {
            return Array.Empty<OPCNodeAttribute>();
        }

        for (int ii = 0; ii < references.Count; ii++)
        {
            // ignore external references.
            if (references[ii].NodeId.IsAbsolute)
            {
                continue;
            }

            ReadValueId nodeToRead = new()
            {
                NodeId = (NodeId)references[ii].NodeId,
                AttributeId = Attributes.Value
            };
            nodesToRead.Add(nodeToRead);
        }

        // 读取当前的值
        var result = await m_session.ReadAsync(
             null,
             0,
             TimestampsToReturn.Neither,
             nodesToRead,
             cancellationToken).ConfigureAwait(false);
        var results = result.Results;
        var diagnosticInfos = result.DiagnosticInfos;

        ClientBase.ValidateResponse(results, nodesToRead);
        ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);

        // process results.

        List<OPCNodeAttribute> nodeAttribute = new();
        for (int ii = 0; ii < results.Count; ii++)
        {
            OPCNodeAttribute item = new();

            // process attribute value.
            if (ii < startOfProperties)
            {
                // ignore attributes which are invalid for the node.
                if (results[ii].StatusCode == StatusCodes.BadAttributeIdInvalid)
                {
                    continue;
                }

                // get the name of the attribute.
                item.Name = GetBrowseName(nodesToRead[ii].AttributeId);

                // display any unexpected error.
                if (StatusCode.IsBad(results[ii].StatusCode))
                {
                    item.Type = Utils.Format("{0}", Attributes.GetDataTypeId(nodesToRead[ii].AttributeId));
                    item.Value = Utils.Format("{0}", results[ii].StatusCode);
                }

                // display the value.
                else
                {
                    TypeInfo typeInfo = TypeInfo.Construct(results[ii].Value);

                    item.Type = typeInfo.BuiltInType.ToString();

                    if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
                    {
                        item.Type += "[]";
                    }

                    item.Value = results[ii].Value;//Utils.Format("{0}", results[ii].Value);
                }
            }

            // process property value.
            else
            {
                // ignore properties which are invalid for the node.
                if (results[ii].StatusCode == StatusCodes.BadNodeIdUnknown)
                {
                    continue;
                }

                // get the name of the property.
                item.Name = Utils.Format("{0}", references[ii - startOfProperties]);

                // display any unexpected error.
                if (StatusCode.IsBad(results[ii].StatusCode))
                {
                    item.Type = String.Empty;
                    item.Value = Utils.Format("{0}", results[ii].StatusCode);
                }

                // display the value.
                else
                {
                    TypeInfo typeInfo = TypeInfo.Construct(results[ii].Value);

                    item.Type = typeInfo.BuiltInType.ToString();

                    if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
                    {
                        item.Type += "[]";
                    }

                    item.Value = results[ii].Value;
                }
            }

            nodeAttribute.Add(item);
        }

        return nodeAttribute.ToArray();
    }
    /// <summary>
    /// Returns the browse name for the attribute.
    /// </summary>
    public static string GetBrowseName(uint identifier)
    {
        return s_attributesIdToName.Value.TryGetValue(identifier, out string name)
            ? name : string.Empty;
    }
    /// <summary>
    /// Creates a dictionary of identifiers to browse names for the attributes.
    /// </summary>
    private static readonly Lazy<IReadOnlyDictionary<long, string>> s_attributesIdToName =
        new(() =>
        {
            System.Reflection.FieldInfo[] fields = typeof(Attributes).GetFields(
                System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);

            var keyValuePairs = new Dictionary<long, string>();
            foreach (System.Reflection.FieldInfo field in fields)
            {
                keyValuePairs.TryAdd(Convert.ToInt64(field.GetValue(typeof(Attributes))), field.Name);
            }
#if NET8_0_OR_GREATER
            return keyValuePairs.ToFrozenDictionary();
#else
            return new System.Collections.ObjectModel.ReadOnlyDictionary<long, string>(keyValuePairs);
#endif
        });
    /// <summary>
    /// 移除所有的订阅消息
    /// </summary>
    public async Task RemoveAllSubscriptionAsync()
    {
        foreach (var item in _subscriptionDicts)
        {
            await item.Value.DeleteAsync(true).ConfigureAwait(false);
            await m_session.RemoveSubscriptionAsync(item.Value).ConfigureAwait(false);
            try { item.Value.Dispose(); } catch { }
        }
        _subscriptionDicts.Clear();
    }

    /// <summary>
    /// 移除订阅消息
    /// </summary>
    /// <param name="subscriptionName">组名称</param>
    public async Task RemoveSubscriptionAsync(string subscriptionName)
    {
        if (_subscriptionDicts.TryGetValue(subscriptionName, out var subscription))
        {
            // remove
            await subscription.DeleteAsync(true).ConfigureAwait(false);
            await m_session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false);
            try { subscription.Dispose(); } catch { }
            _subscriptionDicts.TryRemove(subscriptionName, out _);
        }
    }

    /// <summary>
    /// 异步写opc标签
    /// </summary>
    public async Task<Dictionary<string, Tuple<bool, string>>> WriteNodeAsync(Dictionary<string, JToken> writeInfoLists, CancellationToken cancellationToken = default)
    {
        Dictionary<string, Tuple<bool, string>> results = new();
        try
        {
            WriteValueCollection valuesToWrite = new();
            foreach (var item in writeInfoLists)
            {
                WriteValue valueToWrite = new()
                {
                    NodeId = new NodeId(item.Key),
                    AttributeId = Attributes.Value,
                };
                var variableNode = await ReadNodeAsync(item.Key, false, false, cancellationToken).ConfigureAwait(false);
                var dataValue = JsonUtils.Decode(
                    m_session.MessageContext,
                    variableNode.DataType,
                   await TypeInfo.GetBuiltInTypeAsync(variableNode.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false),
                    item.Value.CalculateActualValueRank(),
                    item.Value
                    );
                valueToWrite.Value = dataValue;

                valuesToWrite.Add(valueToWrite);
            }

            var result = await m_session.WriteAsync(
     requestHeader: null,
     nodesToWrite: valuesToWrite, cancellationToken).ConfigureAwait(false);

            ClientBase.ValidateResponse(result.Results, valuesToWrite);
            ClientBase.ValidateDiagnosticInfos(result.DiagnosticInfos, valuesToWrite);

            var keys = writeInfoLists.Keys.ToList();
            for (int i = 0; i < keys.Count; i++)
            {
                if (!StatusCode.IsGood(result.Results[i]))
                    results.Add(keys[i], Tuple.Create(false, result.Results[i].ToString()));
                else
                {
                    results.Add(keys[i], Tuple.Create(true, "Success"));
                }
            }

            return results;
        }
        catch (Exception ex)
        {
            var keys = writeInfoLists.Keys.ToList();
            foreach (var item in keys)
            {
                results.Add(item, Tuple.Create(false, ex.Message));
            }
            return results;
        }
    }

    private void Callback(MonitoredItem monitoreditem, MonitoredItemNotificationEventArgs monitoredItemNotificationEventArgs)
    {
        try
        {
            if (m_session != null)
            {
                foreach (var value in monitoreditem.DequeueValues())
                {

                    var variableNode = ReadNode(monitoreditem.StartNodeId.ToString(), false, StatusCode.IsGood(value.StatusCode)).GetAwaiter().GetResult();
                    if (value.Value != null)
                    {
                        if (value.Value.GetType().IsRichPrimitive())
                        {
                            DataChangedHandler?.Invoke((variableNode, monitoreditem, value, null));
                            continue;
                        }

                        var data = JsonUtils.Encode(m_session.MessageContext, TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable), value.Value);
                        if (data == null && value.Value != null)
                        {
                            Log(3, null, $"{monitoreditem.StartNodeId}Conversion error, original value is{value.Value}");
                            var data1 = JsonUtils.Encode(m_session.MessageContext, TypeInfo.GetBuiltInType(variableNode.DataType, m_session.SystemContext.TypeTable), value.Value);
                        }
                        DataChangedHandler?.Invoke((variableNode, monitoreditem, value, data!));
                    }
                    else
                    {
                        var data = JValue.CreateNull();
                        DataChangedHandler?.Invoke((variableNode, monitoreditem, value, data));
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Log(3, ex, $"{monitoreditem.StartNodeId} - Subscription processing error");
        }
    }

    #endregion 订阅

    #region 连接

    /// <summary>
    /// 检查/创建证书
    /// </summary>
    /// <returns></returns>
    public async Task CheckApplicationInstanceCertificate()
    {
        await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200).ConfigureAwait(false);
    }

    SemaphoreSlim waitLock = new(1, 1);

    private string LastServerUrl { get; set; }
    /// <summary>
    /// Creates a new session.
    /// </summary>
    /// <returns>The new session object.</returns>
    private async Task ConnectAsync(string serverUrl, CancellationToken cancellationToken)
    {
        if (m_session != null)
        {
            return;
        }
        try
        {
            await waitLock.WaitAsync(cancellationToken).ConfigureAwait(false);
            if (m_session != null)
            {
                return;
            }
            await PrivateDisconnectAsync().ConfigureAwait(false);
            if (LastServerUrl != serverUrl)
            {
                _variableDicts.Clear();
            }
            LastServerUrl = serverUrl;
            if (m_configuration == null)
            {
                throw new ArgumentNullException(nameof(m_configuration));
            }
            var useSecurity = OpcUaProperty?.UseSecurity ?? true;

            EndpointDescription endpointDescription = await OpcUaUtils.SelectEndpointAsync(m_configuration, serverUrl, useSecurity, 10000).ConfigureAwait(false);
            EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration);
            ConfiguredEndpoint endpoint = new(null, endpointDescription, endpointConfiguration);
            UserIdentity userIdentity;
            if (!string.IsNullOrEmpty(OpcUaProperty.UserName))
            {
                userIdentity = new UserIdentity(OpcUaProperty.UserName, OpcUaProperty.Password);
            }
            else
            {
                userIdentity = new UserIdentity(new AnonymousIdentityToken());
            }
            //创建本地证书
            if (useSecurity)
                await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200, cancellationToken).ConfigureAwait(false);

            m_session = await Opc.Ua.Client.Session.CreateAsync(
                DefaultSessionFactory.Instance,
            m_configuration,
            (ITransportWaitingConnection)null,
            endpoint,
            false,
            OpcUaProperty.CheckDomain,
            (string.IsNullOrEmpty(OPCUAName)) ? m_configuration.ApplicationName : OPCUAName,
            600000,
            userIdentity,
            null, cancellationToken
            ).ConfigureAwait(false);
            connected = true;

            m_session.KeepAliveInterval = OpcUaProperty.KeepAliveInterval == 0 ? 60000 : OpcUaProperty.KeepAliveInterval;
            m_session.KeepAlive += Session_KeepAlive;

            typeSystem = new ComplexTypeSystem(m_session);

            // raise an event.
            DoConnectComplete(true);

            Log(2, null, "Connected");
        }
        finally
        {
            waitLock.Release();
        }
    }

    private async Task PrivateDisconnectAsync()
    {
        bool state = m_session?.Connected == true;

        connected = false;
        if (m_reConnectHandler != null)
        {
            try { m_reConnectHandler.Dispose(); } catch { }
            m_reConnectHandler = null;
        }
        if (m_session != null)
        {
            m_session.KeepAlive -= Session_KeepAlive;
            await m_session.CloseAsync(10000).ConfigureAwait(false);
            m_session.Dispose();
            m_session = null;
        }

        if (state)
        {
            Log(2, null, "Disconnected");
            DoConnectComplete(false);
        }
    }

    #endregion 连接

    #region 读取/写入

    /// <summary>
    /// 从服务器读取值
    /// </summary>
    private async Task<List<(string, DataValue, JToken)>> ReadJTokenValueAsync(NodeId[] nodeIds, CancellationToken cancellationToken = default)
    {
        if (m_session == null)
        {
            throw new ArgumentNullException(nameof(m_session));
        }
        ReadValueIdCollection nodesToRead = new();
        for (int i = 0; i < nodeIds.Length; i++)
        {
            nodesToRead.Add(new ReadValueId()
            {
                NodeId = nodeIds[i],
                AttributeId = Attributes.Value
            });
        }

        // 读取当前的值
        var result = await m_session.ReadAsync(
             null,
             0,
             TimestampsToReturn.Both,
             nodesToRead,
             cancellationToken).ConfigureAwait(false);
        var results = result.Results;
        var diagnosticInfos = result.DiagnosticInfos;
        ClientBase.ValidateResponse(results, nodesToRead);
        ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
        List<(string, DataValue, JToken)> jTokens = new();
        for (int i = 0; i < results.Count; i++)
        {
            var variableNode = await ReadNodeAsync(nodeIds[i].ToString(), false, StatusCode.IsGood(results[i].StatusCode), cancellationToken).ConfigureAwait(false);
            var type = await TypeInfo.GetBuiltInTypeAsync(variableNode.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false);
            var jToken = JsonUtils.Encode(m_session.MessageContext, type, results[i].Value);
            jTokens.Add((variableNode.NodeId.ToString(), results[i], jToken));
        }
        return jTokens.ToList();
    }

    /// <summary>
    /// 从服务器或缓存读取节点
    /// </summary>
    private async Task<VariableNode> ReadNode(string nodeIdStr, bool isOnlyServer = true, bool cache = true, CancellationToken cancellationToken = default)
    {
        if (!isOnlyServer)
        {
            if (_variableDicts.TryGetValue(nodeIdStr, out var value))
            {
                return value;
            }
        }
        NodeId nodeToRead = new(nodeIdStr);

        uint[] attributes = new uint[] { Attributes.NodeId, Attributes.DataType };

        // build list of values to read.
        ReadValueIdCollection itemsToRead = new ReadValueIdCollection();
        foreach (uint attributeId in attributes)
        {
            ReadValueId itemToRead = new ReadValueId
            {
                NodeId = nodeToRead,
                AttributeId = attributeId
            };
            itemsToRead.Add(itemToRead);
        }

        // read from server.

        var result = await m_session.ReadAsync(
             null,
             0,
             TimestampsToReturn.Neither,
             itemsToRead,
             cancellationToken).ConfigureAwait(false);
        var values = result.Results;
        var diagnosticInfos = result.DiagnosticInfos;
        var responseHeader = result.ResponseHeader;


        VariableNode variableNode = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader).FirstOrDefault();

        if (cache)
            _variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
        return variableNode;
    }

    /// <summary>
    /// 从服务器或缓存读取节点
    /// </summary>
    private async Task<VariableNode> ReadNodeAsync(string nodeIdStr, bool isOnlyServer = true, bool cache = true, CancellationToken cancellationToken = default)
    {
        if (!isOnlyServer)
        {
            if (_variableDicts.TryGetValue(nodeIdStr, out var value))
            {
                return value;
            }
        }

        NodeId nodeToRead = new(nodeIdStr);

        uint[] attributes = new uint[] { Attributes.NodeId, Attributes.DataType };

        // build list of values to read.
        ReadValueIdCollection itemsToRead = new ReadValueIdCollection();
        foreach (uint attributeId in attributes)
        {
            ReadValueId itemToRead = new ReadValueId
            {
                NodeId = nodeToRead,
                AttributeId = attributeId
            };
            itemsToRead.Add(itemToRead);
        }

        // read from server.
        ReadResponse readResponse = await m_session.ReadAsync(
            null,
            0,
            TimestampsToReturn.Neither,
            itemsToRead, cancellationToken).ConfigureAwait(false);

        DataValueCollection values = readResponse.Results;
        DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
        var responseHeader = readResponse.ResponseHeader;
        VariableNode variableNode = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader).FirstOrDefault();

        if (OpcUaProperty.LoadType && variableNode.DataType != NodeId.Null && (await TypeInfo.GetBuiltInTypeAsync(variableNode.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false)) == BuiltInType.ExtensionObject)
            await typeSystem.LoadTypeAsync(variableNode.DataType, ct: cancellationToken).ConfigureAwait(false);

        if (cache)
            _variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
        return variableNode;
    }

    private List<VariableNode> GetVariableNodes(ReadValueIdCollection itemsToRead, DataValueCollection values, DiagnosticInfoCollection diagnosticInfos, ResponseHeader responseHeader, int count = 1)
    {
        ClientBase.ValidateResponse(values, itemsToRead);
        ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
        List<VariableNode> variableNodes = new();
        for (int i = 0; i < count; i++)
        {
            VariableNode variableNode = new VariableNode();
            DataValue value = values[1 + 2 * i];

            //if (!DataValue.IsGood(value))
            //{
            //    //throw ServiceResultException.Create(values[1+2 * i].StatusCode, 1+2 * i, diagnosticInfos, responseHeader.StringTable);
            //}
            // DataType Attribute

            if (value == null)
            {
                //throw ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Variable does not support the DataType attribute.");
                variableNode.DataType = NodeId.Null;
            }
            else
            {
                variableNode.DataType = (NodeId)value.GetValue(typeof(NodeId));
            }
            value = values[0 + 2 * i];

            // NodeId Attribute
            if (!DataValue.IsGood(value))
            {
                Log(3, ServiceResultException.Create(value.StatusCode, 0 + 2 * i, diagnosticInfos, responseHeader.StringTable), $"Get nodeid {itemsToRead[0 + 2 * i].NodeId} fail");
            }
            if (value == null)
            {
                Log(3, ServiceResultException.Create(StatusCodes.BadUnexpectedError, "Node does not support the NodeId attribute."), $"Get nodeid {itemsToRead[0 + 2 * i].NodeId} fail");
            }

            variableNode.NodeId = (NodeId)value.GetValue(typeof(NodeId));
            variableNodes.Add(variableNode);
        }

        return variableNodes;
    }

    /// <summary>
    /// 从服务器读取节点
    /// </summary>
    private async Task<List<Node>> ReadNodesAsync(string[] nodeIdStrs, bool cache = false, CancellationToken cancellationToken = default)
    {
        List<Node> result = new(nodeIdStrs.Length);
        foreach (var items in nodeIdStrs.ChunkBetter(OpcUaProperty.GroupSize))
        {
            uint[] attributes = new uint[] { Attributes.NodeId, Attributes.DataType };

            // build list of values to read.
            ReadValueIdCollection itemsToRead = new ReadValueIdCollection();
            foreach (var item in items)
            {
                foreach (uint attributeId in attributes)
                {
                    ReadValueId itemToRead = new ReadValueId
                    {
                        NodeId = item,
                        AttributeId = attributeId
                    };
                    itemsToRead.Add(itemToRead);
                }
            }

            // read from server.
            ReadResponse readResponse = await m_session.ReadAsync(
                null,
                0,
                TimestampsToReturn.Neither,
                itemsToRead, cancellationToken).ConfigureAwait(false);

            DataValueCollection values = readResponse.Results;
            DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
            var responseHeader = readResponse.ResponseHeader;

            var variableNodes = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader, nodeIdStrs.Length);

            for (int i = 0; i < variableNodes.Count; i++)
            {
                var node = variableNodes[i];
                if (_variableDicts.TryGetValue(nodeIdStrs[i], out var value))
                {
                }
                else
                {
                    if (cache)
                        _variableDicts.AddOrUpdate(nodeIdStrs[i], a => node, (a, b) => node);
                    if (node.DataType != NodeId.Null && (await TypeInfo.GetBuiltInTypeAsync(node.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false)) == BuiltInType.ExtensionObject)
                    {
                        await typeSystem.LoadTypeAsync(node.DataType, ct: cancellationToken).ConfigureAwait(false);
                    }
                }
                result.Add(node);
            }
        }

        return result;
    }

    #endregion 读取/写入

    #region 私有方法

    private void CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs eventArgs)
    {
        if (ServiceResult.IsGood(eventArgs.Error))
            eventArgs.Accept = true;
        else if (OpcUaProperty.AutoAcceptUntrustedCertificates)
            eventArgs.Accept = true;
        else
            throw new Exception(string.Format("Verification certificate failed with error code: {0}: {1}", eventArgs.Error.Code, eventArgs.Error.AdditionalInfo));
    }

    /// <summary>
    /// Raises the connect complete event on the main GUI thread.
    /// </summary>
    private void DoConnectComplete(bool state)
    {
        m_ConnectComplete?.Invoke(this, state);
    }

    /// <summary>
    /// Report the client status
    /// </summary>
    /// <param name="logLevel">Whether the status represents an error. </param>
    /// <param name="exception">exception</param>
    /// <param name="status">The status message.</param>
    /// <param name="args">Arguments used to format the status message.</param>
    private void Log(byte logLevel, Exception exception, string status, params object[] args)
    {
        LogEvent?.Invoke(logLevel, this, string.Format(status, args), exception);
    }

    private async Task<Dictionary<string, List<OPCNodeAttribute>>> ReadNoteAttributeAsync(BrowseDescriptionCollection nodesToBrowse, ReadValueIdCollection nodesToRead, CancellationToken cancellationToken)
    {
        int startOfProperties = nodesToRead.Count;

        ReferenceDescriptionCollection references = await OpcUaUtils.BrowseAsync(m_session, nodesToBrowse, false, cancellationToken).ConfigureAwait(false);

        if (references == null)
        {
            throw new("浏览失败");
        }

        for (int ii = 0; ii < references.Count; ii++)
        {
            if (references[ii].NodeId.IsAbsolute)
            {
                continue;
            }

            ReadValueId nodeToRead = new()
            {
                NodeId = (NodeId)references[ii].NodeId,
                AttributeId = Attributes.Value
            };
            nodesToRead.Add(nodeToRead);
        }

        var result = await m_session.ReadAsync(
            null,
            0,
            TimestampsToReturn.Neither,
            nodesToRead, cancellationToken).ConfigureAwait(false);

        ClientBase.ValidateResponse(result.Results, nodesToRead);
        ClientBase.ValidateDiagnosticInfos(result.DiagnosticInfos, nodesToRead);

        Dictionary<string, List<OPCNodeAttribute>> nodeAttributes = new();
        for (int ii = 0; ii < result.Results.Count; ii++)
        {
            DataValue nodeValue = result.Results[ii];
            var nodeToRead = nodesToRead[ii];
            OPCNodeAttribute item = new();
            if (ii < startOfProperties)
            {
                if (nodeValue.StatusCode == StatusCodes.BadAttributeIdInvalid)
                {
                    continue;
                }

                item.Name = GetBrowseName(nodesToRead[ii].AttributeId);
                if (StatusCode.IsBad(nodeValue.StatusCode))
                {
                    item.Type = Utils.Format("{0}", Attributes.GetDataTypeId(nodesToRead[ii].AttributeId));
                    item.Value = Utils.Format("{0}", nodeValue.StatusCode);
                }
                else
                {
                    TypeInfo typeInfo = TypeInfo.Construct(nodeValue.Value);
                    item.Type = typeInfo.BuiltInType.ToString();

                    if (typeInfo.ValueRank >= ValueRanks.OneOrMoreDimensions)
                    {
                        item.Type += "[]";
                    }
                    if (item.Name == nameof(Attributes.NodeClass))
                    {
                        item.Value = ((NodeClass)nodeValue.Value).ToString();
                    }
                    else if (item.Name == nameof(Attributes.EventNotifier))
                    {
                        item.Value = ((EventNotifierType)nodeValue.Value).ToString();
                    }
                    else
                        item.Value = nodeValue.Value;
                }
            }

            if (nodeAttributes.ContainsKey(nodeToRead.NodeId.ToString()))
            {
                nodeAttributes[nodeToRead.NodeId.ToString()].Add(item);
            }
            else
            {
                nodeAttributes.Add(nodeToRead.NodeId.ToString(), new() { item });
            }
        }
        return nodeAttributes;
    }

    /// <summary>
    /// 连接处理器连接事件处理完成。
    /// </summary>
    private void Server_ReconnectComplete(object? sender, EventArgs e)
    {
        try
        {
            Log(2, null, "Reconnected : success");

            if (!Object.ReferenceEquals(sender, m_reConnectHandler))
            {
                return;
            }
            // if session recovered, Session property is null
            if (m_reConnectHandler.Session != null)
            {
                // ensure only a new instance is disposed
                // after reactivate, the same session instance may be returned
                if (!Object.ReferenceEquals(m_session, m_reConnectHandler.Session))
                {
                    var session = m_session;
                    m_session = m_reConnectHandler.Session;
                    connected = true;

                    Utils.SilentDispose(session);
                }
            }

            m_reConnectHandler.Dispose();
            m_reConnectHandler = null;

            // raise any additional notifications.
            m_ReconnectComplete?.Invoke(this, e);
        }
        catch (Exception ex)
        {
            Log(3, ex, $"{nameof(Server_ReconnectComplete)}");
        }
    }

    private void Session_KeepAlive(ISession session, KeepAliveEventArgs e)
    {
        lock (checkLock)
        {
            if (!Object.ReferenceEquals(session, m_session))
            {
                return;
            }

            if (ServiceResult.IsBad(e.Status))
            {
                connected = false;

                if (m_reConnectHandler == null)
                {
                    Log(3, null, "Reconnecting : {0}", e.Status.ToString());
                    m_ReconnectStarting?.Invoke(this, e);

                    m_reConnectHandler = new SessionReconnectHandler(true, 10000);
                    m_reConnectHandler.BeginReconnect(m_session, 1000, Server_ReconnectComplete);

                    e.CancelKeepAlive = true;
                }
            }

            // raise any additional notifications.
            m_KeepAliveComplete?.Invoke(this, e);
        }
    }



    #endregion 私有方法
}
